package com.snda.sc.store;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.FileCopyUtils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.jolbox.bonecp.BoneCP;
import com.jolbox.bonecp.BoneCPConfig;
import com.snda.sc.exception.ConfigureException;
import com.snda.sc.exception.SCException;
import com.snda.sc.utils.Charsets;
import com.snda.sc.utils.CommonUtils;
import com.snda.sc.utils.Logs;

@Component("databaseStore")
public class DatabaseStore implements IDatabaseStore {

	public static final String JDBC_CONFIG_FILENAME = "store.json";
	public static final int WORKER_SIZE = 3;

	@Value("${sc.plt}")
	private String plt;

	private BoneCP pool;
	private String tableName;
	private String saveSql;

	private BlockingQueue<LoginRecord> recordQueue = new ArrayBlockingQueue<>(1024);

	@PostConstruct
	public void init() {

		tableName = plt + "_logins_records";
		saveSql = "INSERT INTO " + tableName + "(plt, sns_uid, uid, pt, time, app_name) VALUES(?, ?, ?, ?, ? ,?)";

		for (int i = 0; i < WORKER_SIZE; i++)
			new Thread(new SaveWorker(), "Database-Save-Worker" + i).start();
	}

	@Override
	public void configure(File confFile) throws Exception {
		JSONObject conf = JSON.parseObject(new String(Files.readAllBytes(confFile.toPath()), Charsets.UTF_8));
		JSONObject jdbcConf = conf.getJSONObject("jdbc");
		if (jdbcConf == null) throw new ConfigureException("JDBC config node is undefined in store.json.");
		pool = createPool(jdbcConf);
		Logs.logSysInfo("Jdbc connected, use config: {}", jdbcConf);

		try {
			createTable();
		} catch (Exception e) {
			throw new SCException("Create table: " + tableName + " error.", e);
		}
	}

	@Override
	public Set<String> getConfigFileNames() {
		return CommonUtils.initSet(JDBC_CONFIG_FILENAME);
	}

	@Override
	public boolean watchModify() {
		return false;
	}

	@Override
	public Scope getScope() {
		return Scope.GLOBAL;
	}

	@Override
	public void record(LoginRecord loginRecord) {
		if (!recordQueue.offer(loginRecord)) Logs.logSysInfo("LoginRecord task queue is full, {} is discarded.", loginRecord);
	}

	@PreDestroy
	public void destroy() {
		if (pool != null) pool.shutdown();
	}

	private class SaveWorker implements Runnable {
		@Override
		public void run() {
			while (true) {
				LoginRecord loginRecord = null;
				try {
					loginRecord = recordQueue.take();
				} catch (InterruptedException e1) {
					Thread.currentThread().interrupt();
				}
				try (Connection con = pool.getConnection(); PreparedStatement pstmt = con.prepareStatement(saveSql)) {
					pstmt.setString(1, loginRecord.getPlt());
					pstmt.setString(2, loginRecord.getSnsUid());
					pstmt.setString(3, loginRecord.getUid());
					pstmt.setString(4, loginRecord.getPt());
					pstmt.setTimestamp(5, loginRecord.getTime());
					pstmt.setString(6, loginRecord.getAppName());
					pstmt.executeUpdate();
				} catch (Exception e) {
					Logs.logException("Save login record in mysql error.", e);
				}
			}
		}
	}

	private void createTable() throws SQLException, IOException {
		try (Connection con = pool.getConnection()) {
			if (checkTableExist(con)) return;
			String tableSql = new String(FileCopyUtils.copyToByteArray(CommonUtils
					.getStreamByClassLoader("com/snda/sc/store/CREATE_TABLE")), Charsets.UTF_8).replace("${tableName}",
					tableName);
			try (Statement stmt = con.createStatement()) {
				stmt.executeUpdate(tableSql);
				Logs.logSysInfo("Create sc login record table: {}.", tableName);
			}
		}
	}

	private boolean checkTableExist(Connection con) throws SQLException {
		DatabaseMetaData meta = con.getMetaData();
		ResultSet tables = meta.getTables(null, "%", tableName, new String[] { "TABLE" });
		return tables.next();
	}

	private BoneCP createPool(JSONObject jsonConfig) throws SQLException {

		/*
		 * TODO BoneCP Bug?
		 * JDK1.6以后就有SPI机制，应该不需要Class.forName这种手动方式加载驱动。但是BoneCP如果没有手动加载就会出现“No
		 * suitable driver found xxx”的异常。这是否与BoneCP有关？
		 */
		try {
			Class.forName("com.mysql.jdbc.Driver");
		} catch (ClassNotFoundException e) {
			Logs.logException("MySQL JDBC Driver is not found.", e);
		}

		BoneCPConfig config = new BoneCPConfig();
		config.setJdbcUrl(jsonConfig.getString("url"));
		config.setUsername(jsonConfig.getString("username"));
		config.setPassword(jsonConfig.getString("password"));
		config.setIdleConnectionTestPeriod(jsonConfig.getLongValue("idleTestPeriod"), TimeUnit.SECONDS);
		config.setIdleMaxAge(jsonConfig.getLongValue("maxIdleAge"), TimeUnit.SECONDS);
		config.setMaxConnectionsPerPartition(jsonConfig.getIntValue("maxConnection"));
		config.setMinConnectionsPerPartition(jsonConfig.getIntValue("minConnection"));
		return new BoneCP(config);
	}
}
